//==========================================================================
// Copyright (c) 2000-2008,  Elastos, Inc.  All Rights Reserved.
//==========================================================================

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <psapi.h>
#include <direct.h>

const char *c_pszCopyright = \
    "ElAura Version 1.0, Revision 3\n" \
    "Copyright (c) 2000-2008, Elastos, Inc. All rights reserved.\n\n";

const char *c_pszUsage = \
    "Usuage: elaura [options] [*.exe/*.dll] [arguments]\n" \
    "\n" \
    "                          -OPTIONS-\n" \
    "-agent [port]          Start agent with given port(default: 1314)\n" \
    "-config <path>         Configuration path of simulator\n" \
    "-verbose               Start elaura with verbose on\n" \
    "-nogui                 Start elservice with no gui\n" \
    "-h, -?                 Display this help message\n";

inline bool IsCommandSwitch(char c)
{
    return ('/' == c || '-' == c);
}

class ArgsStream
{
public:
    inline void Initialize(int nArgc, char *ppArgv[]);

    inline char PeekChar();

    inline char GetChar();
    inline char *GetWord();
    inline int GetIndex();

private:
    int m_nArgc;
    char **m_ppArgv;

    int m_x, m_y;
};

void ArgsStream::Initialize(int nArgc, char **ppArgv)
{
    m_nArgc = nArgc;
    m_ppArgv = ppArgv;
    m_x = 1;
    m_y = 0;
}

char ArgsStream::PeekChar()
{
    return (m_x < m_nArgc ? m_ppArgv[m_x][m_y] : 0);
}

char ArgsStream::GetChar()
{
    char ch;

    if (m_x < m_nArgc) {
        ch = m_ppArgv[m_x][m_y];
        if (0 == m_ppArgv[m_x][++m_y]) {
            m_x++;
            m_y = 0;
        }
        return ch;
    }
    return 0;
}

char *ArgsStream::GetWord()
{
    char *pszWord;

    if (m_x < m_nArgc) {
        pszWord = &(m_ppArgv[m_x][m_y]);
        m_x++;
        m_y = 0;
        return pszWord;
    }
    return NULL;
}

/*
 * The beginning index of application arguments
 */
int ArgsStream::GetIndex()
{
    return m_x;
}

/*
 * Execute given command
 */
BOOL ExecuteCommand(char* cmdline, int flag, STARTUPINFO *si, PROCESS_INFORMATION *pi, bool bVerbose)
{
    ZeroMemory(si, sizeof(STARTUPINFO));
    si->cb = sizeof(STARTUPINFO);
    ZeroMemory(pi, sizeof(PROCESS_INFORMATION));
    if (bVerbose) {
        si->dwYCountChars = 5000;
        si->dwFlags |= STARTF_USECOUNTCHARS;
    }
    else {
        si->dwFlags |= STARTF_USESTDHANDLES;
        // be quiet!
        si->hStdOutput = NULL;
        si->hStdInput = NULL;
        si->hStdError = NULL;
    }

    return CreateProcess(NULL,
            cmdline,     /* command line */
            NULL,    /* Security */
            NULL,    /* thread */
            TRUE,    /* inherit handles */
            flag,    /* start flags */
            NULL,    /* winenv, */
            NULL,    /* current directory */
            si, pi);
}

/*
 * Whether given argument need "..." wrapper
 */
bool NeedQuotes(char* str)
{
    int nLength = strlen(str);
    for (int i = 0; i < nLength; i++) {
        char ch = str[i];
        if (ch == ' ') {
            return true;
        }
    }
    return false;
}

/*
 * "\"" inside an argument needs "\\"
 */
void EnsureWrappingInto(char* str, char* target)
{
    int nStartIndex = strlen(target);
    int nLength = strlen(str);
    for (int i = 0; i < nLength; i++) {
        char ch = str[i];
        if (ch == '\"') {
            target[nStartIndex] = '\\';
            nStartIndex++;
        }
        target[nStartIndex] = ch;
        nStartIndex++;
    }
    target[nStartIndex] = '\0';
}

/*
 * Wrap command line arguments into a string variable
 */
void WrapArguments(int argc, char **argv, int nStartArgsIndex, char* pszArgs)
{
    if (nStartArgsIndex != -1) {
        int nArgsCount = argc - nStartArgsIndex;
        if (nArgsCount > 0) {
            for (int k = 0; k < nArgsCount; k++) {
                char* a = argv[nStartArgsIndex + k];
                bool q = NeedQuotes(a);
                if (q) {
                    strcat(pszArgs, "\"");
                    EnsureWrappingInto(a, pszArgs);
                    strcat(pszArgs, "\"");
                }
                else {
                    EnsureWrappingInto(a, pszArgs);
                }
                if (k != nArgsCount - 1) {
                    strcat(pszArgs, " ");
                }
            }
        }
    }
}

STARTUPINFO g_server_si, g_services_si;
PROCESS_INFORMATION g_server_pi, g_services_pi;

bool g_bStartServerProcessInside = true;
bool g_bStartServicesProcessInside = true;
bool g_bStartAgentProcessInside = true;

HANDLE g_hEventBeNotifiedToExit = NULL;
HANDLE g_hEventToNotifyStartup  = NULL;

/*
 * bVerbose == true, create new console
 */
int StartPALServer(bool bVerbose)
{
    int flag = (bVerbose ? CREATE_NEW_CONSOLE : 0);
    int ret = ExecuteCommand("elcentral.exe", flag, &g_server_si, &g_server_pi, true);
    if (!ret) {
        fprintf(stderr, "[ERROR] elaura (0x0101) : Start elcentral.exe failed (%d)\n", GetLastError());
        return 1;
    }

    ////fprintf(stdout, "[INFO] elaura (0x0201) : Waiting elcentral to be started ...\n");
    HANDLE hPalServerStartedEvent = CreateEventW(NULL, FALSE, FALSE, L"ElCentralStarted");
    if (WaitForSingleObject(hPalServerStartedEvent, 10000) == WAIT_TIMEOUT) { // wait for 10s
        fprintf(stderr, "[ERROR] elaura (0x0102) : Start elcentral.exe timeout.\n");
        return 1;
    }
    ////fprintf(stdout, "[INFO] elaura (0x0202) : elcentral started.\n");
    return 0;
}

/*
 * bVerbose == true, create new console
 */
int StartPlatServices(bool bVerbose, char* pszConfigPath, char* pszParam)
{
    int flag = (bVerbose ? CREATE_NEW_CONSOLE : 0);
    if (pszConfigPath != NULL) {
    	int cfgLength = strlen(pszConfigPath);
    	char lastChar = pszConfigPath[cfgLength - 1];
    	if (lastChar == '/' || lastChar == '\\') {
    		pszConfigPath[cfgLength - 1] = '\0';
    		cfgLength--;
    	}
    	int length = cfgLength + 24; // 24 = "XDK_SIMULATOR_RESOURCE=".length + 1
        char* pszEnv = new char[length];
        sprintf(pszEnv, "XDK_SIMULATOR_RESOURCE=%s\0", pszConfigPath);
        _putenv(pszEnv);
        delete[] pszEnv;
    }

	char *pszCmd = NULL;
    if (NULL != pszParam) {
        int nLen = strlen("elservices.exe") + strlen(pszParam) + 1;
        pszCmd = new char[nLen];
        sprintf(pszCmd, "elservices.exe -%s\0", pszParam);
    }
    else {
        pszCmd = new char[strlen("elservices.exe") + 1];
        sprintf(pszCmd, "elservices.exe\0");
    }
    int nRet = ExecuteCommand(pszCmd, flag, &g_services_si, &g_services_pi, bVerbose);
    delete[] pszCmd;
    if (!nRet) {
        fprintf(stderr, "[ERROR] elaura (0x0103) : Start elasvcs.exe failed (%d)\n", GetLastError());
        return 1;
    }
    ////fprintf(stdout, "[INFO] elaura (0x0203) : Waiting platform services to be registered ...\n");
    HANDLE hPlatServicesRegisteredEvent = CreateEventW(NULL, FALSE, FALSE, L"ElServicesRegistered");
    if (WaitForSingleObject(hPlatServicesRegisteredEvent, 30000) == WAIT_TIMEOUT) { // wait for 30s !
        fprintf(stderr, "[ERROR] elaura (0x0104) : Start elasvcs.exe timeout.\n");
        return 1;
    }
    ////fprintf(stdout, "[INFO] elaura (0x0204) : Platform services registered.\n");
    return 0;
}

void TerminatePALServer()
{
    DWORD exitCode;
    if (GetExitCodeProcess(g_server_pi.hProcess, &exitCode)) {
        if (STILL_ACTIVE == exitCode) {
            // Send event to exit elasrv.exe process
            HANDLE hExitPALServerEvent = CreateEventW(NULL, FALSE, FALSE, L"ExitElCentral");
            SetEvent(hExitPALServerEvent);

            CloseHandle(g_server_pi.hProcess);
            CloseHandle(g_server_pi.hThread);
        }
    }
}

void TerminatePlatServices()
{
    DWORD exitCode;
    if (GetExitCodeProcess(g_services_pi.hProcess, &exitCode)) {
        if (STILL_ACTIVE == exitCode) {
#if (_TEST_TYPE == 10) || (_TEST_TYPE == 11)/* Test for ElAura */
            if (!TerminateProcess(g_services_pi.hProcess, 0)) {
                printf("error in terminate elservices errcode: %d\n", GetLastError());
            }
#else
            // Send event to deregister all services and exit elservices.exe process
            HANDLE hDeregisterPlatServicesEvent = CreateEventW(NULL, FALSE, FALSE, L"DeregisterElServices");
            SetEvent(hDeregisterPlatServicesEvent);

            if (WaitForSingleObject(g_services_pi.hProcess, 5000) == WAIT_TIMEOUT) { // wait for 5s !
                fprintf(stderr, "[ERROR] elaura (0x0105) : Platform services can not be terminated!\n");
                //continue to exit
            }
#endif // (_TEST_TYPE == 10) || (_TEST_TYPE == 11)

            CloseHandle(g_services_pi.hProcess);
            CloseHandle(g_services_pi.hThread);
        }
    }
}

void CheckProcesses()
{
    // Get the list of process identifiers.
    DWORD aProcesses[1024], cbNeeded, cProcesses;

    if (!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded)) {
        fprintf(stderr, "[ERROR] elaura (0x0106) : Failed to list all processes.\n");
    }
    else {
        // Calculate how many process identifiers were returned.
        cProcesses = cbNeeded / sizeof(DWORD);
        for (unsigned int i = 0; i < cProcesses; i++) {
            if (aProcesses[i] != 0) {
                DWORD processID = aProcesses[i];
                TCHAR szProcessName[MAX_PATH] = TEXT("<unknown>");
                // Get a handle to the process.
                HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
                                              PROCESS_ALL_ACCESS,
                                              FALSE, processID);
                // Get the process name.
                if (NULL != hProcess) {
                    HMODULE hMod;
                    DWORD cbNeeded;
                    if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
                        &cbNeeded)) {
                        GetModuleBaseName(hProcess, hMod, szProcessName,
                                           sizeof(szProcessName)/sizeof(TCHAR));
                    }
                }
                if (strcmp("elcentral.exe", szProcessName) == 0) {

                    ////fprintf(stdout, "[INFO] elaura (0x0205) : elasrv has already been started.\n");
                    if (!TerminateProcess(hProcess, 0))
                        printf("***%d**", GetLastError());
                    Sleep(1000); //wait until elcentral.exe end, only for test
                }
                else if (strcmp("elservices.exe", szProcessName) == 0) {
                    ////fprintf(stdout, "[INFO] elaura (0x0206) : Services have already been registered.\n");
                    TerminateProcess(hProcess, 0);
                    Sleep(1000); //wait until elservices.exe end, only for test
                }
                else if (strcmp("elacmd.exe", szProcessName) == 0) {
                    ////fprintf(stdout, "[INFO] elaura (0x0207) : Agent has already been started.\n");
                    TerminateProcess(hProcess, 0);
                    Sleep(1000); //wait until elacmd.exe end, only for test
                }
//                else if (strcmp("ezregsvr.exe", szProcessName) == 0) {
//                    fprintf(stdout, "[INFO] elaura (0x0207) : Agent has already been started.\n");
//                    TerminateProcess(hProcess, 0);
//                    Sleep(1000); //wait until ezregsvr.exe end, only for test
//                }

                CloseHandle(hProcess);

                if (!g_bStartServerProcessInside && !g_bStartServicesProcessInside && !g_bStartAgentProcessInside) {
                    // elasrv.exe and elasvcs.exe are already started OUTSIDE!
                    break;
                }
            }
        }
    }
}

BOOL WINAPI CtrlCHandleRoutine(DWORD CtrlType)
{
    static BOOL s_bCtrlCFlag = FALSE;
    if ((CTRL_C_EVENT == CtrlType) && (!s_bCtrlCFlag)) {
        s_bCtrlCFlag = TRUE;
        TerminatePlatServices();
        TerminatePALServer();
        if (NULL != g_hEventBeNotifiedToExit) {
            CloseHandle(g_hEventBeNotifiedToExit);
            g_hEventBeNotifiedToExit = NULL;
        }
        if (NULL != g_hEventToNotifyStartup) {
            CloseHandle(g_hEventToNotifyStartup);
            g_hEventToNotifyStartup = NULL;
        }
    }
    else {
        ExitProcess(0);
    }
    return TRUE;
}

int main(int argc, char **argv)
{
    int nStartArgsIndex = -1;
    char *pszModulePath = NULL;
    char *pszConfigPath = NULL;
    char *pszParam = NULL; //param for elservices
    int nAgentPort = -1;
    bool bVerbose = false;

    /*
     * Begin to parsing arguments
     */
    char c;
    ArgsStream stream;
    stream.Initialize(argc, argv);

    while ((c = stream.PeekChar()) != 0) {
        if (IsCommandSwitch(c)) {
            stream.GetChar(); // skip switch char: '-' or '/'

            c = stream.PeekChar();
            if (IsCommandSwitch(c)) { // another '-'
                stream.GetChar(); // skip switch char: '-'
                c = stream.PeekChar();
            }

            switch (c) {
                case 'a': // agent
                    stream.GetWord(); // skip char "a" or the rest of "agent"
                    c = stream.PeekChar();
                    if ('1' <= c && c <= '9') {
                        nAgentPort = atoi(stream.GetWord());
                    }
                    else {
                        nAgentPort = 1314; // default agent port
                    }
                    break;
                case 'c': // config
                    stream.GetWord(); // skip char "c" or the rest of "config"
                    pszConfigPath = stream.GetWord();
                    break;
                case 'v': // verbose
                    stream.GetWord(); // skip char "v" or the rest of "verbose"
                    bVerbose = true;
                    break;
                case 'n': // nogui
                    pszParam = stream.GetWord(); //skip char "n" or the rest of "nogui"
                    if (0 != strcmp("nogui", pszParam)) {
                    	fputs(c_pszUsage, stdout);
                        return 1;
                    }
                    break;

                case 'h':
                case '?':
                    fputs(c_pszCopyright, stdout);
                    fputs(c_pszUsage, stdout);
                    return 0;

                default:
                    fprintf(stderr, "[ERROR] elaura (0x0001) : arg /%c - unknown switch.\n", c);
                    return 1;
            }
        }
        else {
            pszModulePath = stream.GetWord();
            nStartArgsIndex = stream.GetIndex();
            break;
        }
    }
    /*
     * End of parsing arguments
     */

#if (_TEST_TYPE == 10) || (_TEST_TYPE == 11)
    /*
     * Test for ElAura: Getting max process executing time
     */
    wchar_t *wszMaxTime = _wgetenv(L"TST_MAX_RUNNING_TIME");
    if (NULL == wszMaxTime) {
        fprintf(stderr, "[ERROR] elaura : Env TST_MAX_RUNNING_TIME must be set first.\n");
        return 1;
    }

    wchar_t *p;
    DWORD dwMilliseconds = wcstoul(wszMaxTime, &p, 10);
#else
    DWORD dwMilliseconds = INFINITE;
#endif // (_TEST_TYPE == 10) || (_TEST_TYPE == 11)

    /*
     * Check elasrv.exe and elasvcs.exe
     */
    CheckProcesses();

    /*
     * Lauch elasrv and services processes
     */
    if (g_bStartServerProcessInside) {
        int ret = StartPALServer(bVerbose);
        if (ret != 0) {
            return ret;
        }
    }
    if (g_bStartServicesProcessInside) {
        int ret = StartPlatServices(bVerbose, pszConfigPath, pszParam);
        if (ret != 0) {
            if (g_bStartServerProcessInside) {
                TerminatePALServer();
            }
            return ret;
        }
    }

    if (NULL == g_hEventBeNotifiedToExit) {
         g_hEventBeNotifiedToExit = CreateEventW(NULL,
                                                FALSE,
                                                FALSE,
                                                L"ElAuraNotifyToExitEvent");
         if (NULL == g_hEventBeNotifiedToExit) {
            TerminatePlatServices();
            TerminatePALServer();
            return 1;
         }
    }

    if (NULL == g_hEventToNotifyStartup) {
        // to receive the event from elaura.dll when detaching that
        // elaura should be to exit.
        g_hEventToNotifyStartup = CreateEventW(NULL,
                                                FALSE,
                                                FALSE,
                                                L"ElAuraStartupNotifyEvent");
        if (NULL == g_hEventToNotifyStartup) {
            TerminatePlatServices();
            TerminatePALServer();
            CloseHandle(g_hEventBeNotifiedToExit);
            g_hEventBeNotifiedToExit = NULL;
            return 1;
        }
    }

    if (!SetConsoleCtrlHandler(CtrlCHandleRoutine, TRUE)) {
        printf("Init self Ctrl_C handler fail.\n");
    }

    HANDLE hProcesses[5];
    int nProcessCount = 0;

    BOOL ret = FALSE;
    STARTUPINFO pipe_si;
    PROCESS_INFORMATION pipe_pi;
    if (nAgentPort != -1) {
        char pszArgs[512];
        pszArgs[0] = '\0';
        sprintf(pszArgs, "elswitch.exe --type pipe --port %d", nAgentPort);
        ret = ExecuteCommand(pszArgs, 0, &pipe_si, &pipe_pi, true);
        if (!ret) {
            fprintf(stderr, "[ERROR] elaura (0x0113) : Failed to start elswitch.exe\n");
        }
        hProcesses[nProcessCount] = pipe_pi.hProcess;
        nProcessCount++;
    }

    STARTUPINFO app_si;
    PROCESS_INFORMATION app_pi;

    if (g_bStartServerProcessInside) {
        hProcesses[nProcessCount] = g_server_pi.hProcess;
        nProcessCount++;
    }
    if (g_bStartServicesProcessInside) {
        hProcesses[nProcessCount] = g_services_pi.hProcess;
        nProcessCount++;
    }
    if (NULL != g_hEventBeNotifiedToExit) {
        hProcesses[nProcessCount] = g_hEventBeNotifiedToExit;
        nProcessCount++;
    }

    /*
     * Load application
     */
    if (pszModulePath != NULL || nAgentPort != -1) {
        char pszArgs[32 * 1024];// 32K
        pszArgs[0] = '\0';
        if (nAgentPort != -1) {
            if (!g_bStartAgentProcessInside) {
                fprintf(stderr, "[ERROR] elaura (0x0108) : Agent has already been started\n");
                if (g_bStartServicesProcessInside) {
                    TerminatePlatServices();
                }
                if (g_bStartServerProcessInside) {
                    TerminatePALServer();
                }
                return 1;
            }
            sprintf(pszArgs, "elacmd.exe");
        }
        else {
            if (NeedQuotes(pszModulePath)) {
                strcat(pszArgs, "\"");
                strcat(pszArgs, pszModulePath);
                strcat(pszArgs, "\"");
            }
            else {
                strcat(pszArgs, pszModulePath);
            }
            if (nStartArgsIndex < argc) {
                strcat(pszArgs, " ");
                WrapArguments(argc, argv, nStartArgsIndex, pszArgs);
            }
        }
        ret = ExecuteCommand(pszArgs, 0, &app_si, &app_pi, nAgentPort == -1);
        if (!ret) {
            fprintf(stderr, "[ERROR] elaura (0x0109) : CreateProcess %s failed (%d)\n", pszModulePath, GetLastError());
            if (g_bStartServicesProcessInside) {
                TerminatePlatServices();
            }
            if (g_bStartServerProcessInside) {
                TerminatePALServer();
            }
            return 1;
        }

        if (nAgentPort != -1) {
            ////fprintf(stdout, "[INFO] elaura (0x0210) : Agent is on.\n");
        }

        ////fprintf(stdout, "[INOF] elaura (0x0211) : ElAura is on\n");

        hProcesses[nProcessCount] = app_pi.hProcess;
        nProcessCount++;
    }

    fflush(stdout);

    {
        // to notify ipc mechanism that elaura.exe was startup.
        SetEvent(g_hEventToNotifyStartup);
        CloseHandle(g_hEventToNotifyStartup);
        g_hEventToNotifyStartup = NULL;
    }

    WaitForMultipleObjects(
            nProcessCount,    // number of objects in array
            hProcesses,       // array of objects
            FALSE,            // wait for any
            dwMilliseconds);  // wait for dwMilliseconds

    /*
     * Kill processes and exit
     */
    DWORD exitCode;
    if (pszModulePath != NULL || nAgentPort != -1) {
        if (GetExitCodeProcess(app_pi.hProcess, &exitCode)) {
            if (STILL_ACTIVE == exitCode) {
                if (nAgentPort != -1) {
                    // Just try to kill agent
                    if (!TerminateProcess(app_pi.hProcess, 0)) {
                        fprintf(stderr, "[ERROR] elaura (0x0110) : Failed to terminate agent (%d)\n", GetLastError());
                    }
                }
                else {
#if (_TEST_TYPE == 10) || (_TEST_TYPE == 11) /* Test for ElAura: Killing the process in debug, Windows XP only */
                    char *szCwd = _getcwd(NULL, _MAX_PATH);
                    _chdir("C:/WINDOWS/system32/wbem");
                    system("C:/WINDOWS/system32/taskkill.exe /f /im dwwin.exe");
                    _chdir(szCwd);
#endif // (_TEST_TYPE == 10) || (_TEST_TYPE == 11)

                    // try to kill application
                    if (!TerminateProcess(app_pi.hProcess, 0)) {
                        fprintf(stderr, "[ERROR] elaura (0x0110) : Failed to terminate %s (%d)\n", pszModulePath, GetLastError());
                    }
                }
                CloseHandle(app_pi.hProcess);
                CloseHandle(app_pi.hThread);
            }
        }
    }

    if (g_bStartServicesProcessInside) {
        TerminatePlatServices();
    }
    if (g_bStartServerProcessInside) {
        TerminatePALServer();
    }
    if (NULL != g_hEventBeNotifiedToExit) {
        CloseHandle(g_hEventBeNotifiedToExit);
        g_hEventBeNotifiedToExit = NULL;
    }
    if (nAgentPort != -1
            && GetExitCodeProcess(pipe_pi.hProcess, &exitCode)
            && STILL_ACTIVE == exitCode) {
    	Sleep(100); // wait, elswitch.exe may have its last chance to exit.
        if (GetExitCodeProcess(pipe_pi.hProcess, &exitCode)
                && STILL_ACTIVE == exitCode
                && !TerminateProcess(pipe_pi.hProcess, 0)) {
            printf("[ERROR] elaura (0x0110) : Failed to terminate pipe switch (%d)\n", GetLastError());
        }
    }

    return 0;
}
